//
//  CGLImageCache.m
//  UI26_综合(多线程_ Block_沙盒_MD5_通知中心)
//
//  Created by 李劲成 on 16/4/14.
//  Copyright © 2016年 ljc. All rights reserved.
//

#import "CGLImageCache.h"

#import <CommonCrypto/CommonDigest.h>

#pragma mark - 内存缓存类
@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (instancetype)init {
    self = [super init];
    if (self) {
        // 观察内存警告, 则移除
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object: nil];
    }
    return self;
}

- (void)dealloc {
    // 在 dealloc 时  移除观察
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
@end

#pragma mark - 静态区常量
/** 默认缓存最大缓存时间,  1 week */
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
/** PNG 签名 类型 */
static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
/** PNG 签名 数据 */
static NSData *kPNGSignatureData = nil;

#pragma mark - 函数
/** 声明 */
BOOL ImageDataHasPNGPreffix(NSData *data);
/** 实现 */
BOOL ImageDataHasPNGPreffix(NSData *data) {
    // 获取 PNG 签名长度
#warning ?
    NSUInteger pngSignatureLength = [kPNGSignatureData length];
    if ([data length] >= pngSignatureLength) {
        // 判断 签名 是否相同, 数据是否为 PNG 类型
        if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) {
            return YES;
        }
    }
    return NO;
    
}

FOUNDATION_STATIC_INLINE NSUInteger CGLCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

@interface CGLImageCache ()

#warning ?
@property (nonatomic, strong) NSCache *memCache;
/** 本地缓存路径 */
@property (nonatomic, strong) NSString *diskCachePath;
/** 存放多个自定义路径的数组 */
@property (nonatomic, strong) NSMutableArray *customPaths;
/** 线程 */
@property (CGLDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;

@end



@implementation   CGLImageCache

{
    NSFileManager *_fileManager;
}

#pragma mark - 单例
+ (CGLImageCache *)sharedImageCache {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        // 初始化
        instance = [self new];
        
    });
    return instance;
}
#pragma mark - 初始化
- (instancetype)init {
    return [self initWithSpecificNamespace:@"default"];
}

- (id)initWithSpecificNamespace:(NSString *)nameSpace {
    NSString *path = [self makeDiskCachePath:nameSpace];
    return [self initWithNamespace:nameSpace diskCacheDirectory:path];
}
// core
- (id)initWithNamespace:(NSString *)nameSpace diskCacheDirectory:(NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.CGLWebImageCache." stringByAppendingString:nameSpace];
        // initialise PNG signature data
        kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
        // Create IO serial queue
        _ioQueue = dispatch_queue_create("com.hackemist.CGWebImageCache", DISPATCH_QUEUE_SERIAL);
        // Init default values
        _maxMemoryAge = kDefaultCacheMaxCacheAge;
        
        // Init the memory cache
        _memCache = [[AutoPurgeCache alloc] init];
        _memCache.name = fullNamespace;
        
        // Init the disk cache
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:nameSpace];
            _diskCachePath = path;
        }
        
        // Set decompression to YES
        _shouldDecompressImages = YES;
        
        // memory cache enabled
        _shouldCacheImagesInMemory = YES;
//
//        // Disable iCloud
        _shouldDisableiCloud = YES;
//
        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });
        
#if TARGET_OS_IPHONE
        // Subscribe to app events
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }
    
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    CGLDispatchQueueRelease(_ioQueue);
}

#pragma mark - 添加只读路径
- (void)addReadOnlyCachePath:(NSString *)path {
    if (!self.customPaths) {
        self.customPaths = [NSMutableArray new];
    }
    if (![self.customPaths containsObject:path]) {
        [self.customPaths addObject:path];
    }
}

#pragma mark - 获取路径
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

/** 获取默认路径 */
- (NSString *)defaultCachePathForKey:(NSString *)key {
     return [self cachePathForKey:key inPath:self.diskCachePath];
}
    
#pragma mark CGLImageCache (private)
// core
- (NSString *)cachedFileNameForKey:(NSString *)key {
    // 转换格式
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
#warning ?
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    // <CommonCrypto/CommonDigest.h>
    CC_MD5(str, (CC_LONG)strlen(str), r);
    // pathExtension 将字符串转换成路径格式
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension]isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
    return filename;
}

#pragma mark ImageCache

// Init the disk cache
- (NSString *)makeDiskCachePath:(NSString *)fullNamespace {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingString:fullNamespace];
}

#pragma mark -- 计算 写入本地
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // if memory cache is enabled
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = CGLCacheCostForImage(image);
        // 设置缓存中的指定键的值，并将键值与指定的值关联。
        [self.memCache setObject:image forKey:key cost:cost];
    }
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            
            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // We need to determine if the image is a PNG or a JPEG
                // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                // The first eight bytes of a PNG file always contain the following (decimal) values:
                // 137 80 78 71 13 10 26 10
                
                // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
                // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                // RGB ARGB RGBA
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
                // But if we have an image data, we will look at the preffix
                if ([imageData length] >= [kPNGSignatureData length]) {
                    // 调用函数判断 PNG
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
                
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
#warning ???
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }
            
            if (data) {
                // 判断文件路径是否含有文件
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }
                
                // get cache Path for image key
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // transform to NSUrl
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
                
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
#warning ???
                // disable iCloud backup
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }

            });
    }
  
}

- (void)storeImage:(UIImage *)image forKey:(NSString *)key {
    [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES];
}

- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
    [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk];
}

#pragma mark - 判断本地是否拥有
// 同步
- (BOOL)diskImageExistsWithKey:(NSString *)key {
    BOOL exists = NO;
    
    // this is an exception to access the filemanager on another queue than ioQueue, but we are using the shared instance
    // from apple docs on NSFileManager: The methods of the shared NSFileManager object can be called from multiple threads safely.
    exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
    
    return exists;
}

// 异步
- (void)diskImageExistsWithKey:(NSString *)key completion:(CGLWebImageCheckCacheCompletitionBlock)completionBlock {
    dispatch_async(_ioQueue, ^{
        BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                // 传递 判断结果
                completionBlock(exists);
            });
        }
    });
    
}

#pragma mark - 返回内存图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}
#pragma mark - 返回本地图片
/** 搜索所有路径下的本地图片数据 */
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }
    
    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
    }
    
    return nil;
}

@end
